﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text;
using JetBrains.Application;
using JetBrains.ReSharper.Psi;
using JetBrains.ReSharper.Psi.CSharp;
using JetBrains.ReSharper.Psi.CSharp.Impl;
using JetBrains.ReSharper.Psi.CSharp.Tree;
using JetBrains.ReSharper.Psi.ExtensionsAPI.Tree;
using JetBrains.ReSharper.Psi.Impl.PsiManagerImpl;
using JetBrains.ReSharper.Psi.Resolve;
using JetBrains.ReSharper.Psi.Tree;

namespace Neovolve.Extensibility.ReSharper
{
    /// <summary>
    /// The <see cref="NodeProcessor"/>
    /// class is used to resolve and update nodes that are formattable by the <see cref="ValueTypeAliasFormatter"/>.
    /// </summary>
    internal static class NodeProcessor
    {
        /// <summary>
        /// Ensures the namespace exists.
        /// </summary>
        /// <param name="file">
        /// The file to process.
        /// </param>
        /// <param name="qualifiedNamespaceName">
        /// Name of the qualified namespace.
        /// </param>
        public static void EnsureNamespaceExists(ICSharpFile file, String qualifiedNamespaceName)
        {
            Boolean namespaceExists = false;

            // Loop through the existing imports
            for (Int32 index = 0; index < file.Imports.Count; index++)
            {
                IUsingDirective directive = file.Imports[index];

                IUsingNamespaceDirective namespaceDirective = directive as IUsingNamespaceDirective;

                // We we could convert the type, skip to the next item
                if (namespaceDirective == null
                    || namespaceDirective.ImportedNamespace == null)
                {
                    continue;
                }

                // Check if the using directive is the one we are looking for
                if (namespaceDirective.ImportedNamespace.QualifiedName == qualifiedNamespaceName)
                {
                    // The namespace is already imported.
                    namespaceExists = true;

                    break;
                }
            }

            // Check if the namespace already exists
            if (namespaceExists == false)
            {
                // We need to add the namespace

                // Create the new using statement
                IUsingDirective directive =
                    CSharpElementFactory.GetInstance(file.GetPsiModule()).CreateUsingDirective(
                        qualifiedNamespaceName, null);

                // Add it to the file
                UsingUtil.AddImportTo(file, directive);
            }
        }

        /// <summary>
        /// Resolves the formattable nodes.
        /// </summary>
        /// <param name="node">
        /// The node to process.
        /// </param>
        /// <param name="rangeMarker">
        /// The range marker.
        /// </param>
        /// <param name="option">
        /// The option to use.
        /// </param>
        /// <param name="psiManager">
        /// The psi manager.
        /// </param>
        /// <returns>
        /// A <see cref="Collection{T}"/> instance.
        /// </returns>
        public static IList<ITreeNode> ResolveFormattableNodes(
            ITreeNode node, IPsiRangeMarker rangeMarker, TypeAliasFormat option, PsiManagerImpl psiManager)
        {
            Collection<ITreeNode> nodes = new Collection<ITreeNode>();

            // Resolve the nodes
            ResolveFormattableNodesInternal(node, rangeMarker, option, psiManager, nodes);

            return nodes;
        }

        /// <summary>
        /// Updates the type usage.
        /// </summary>
        /// <param name="node">
        /// The node to process.
        /// </param>
        /// <param name="option">
        /// The option.
        /// </param>
        public static void UpdateTypeUsage(ITreeNode node, TypeAliasFormat option)
        {
            // Create AST fragment for type usage
            IPsiModule project = node.GetPsiModule();
            ITreeNode newNode;

            if (node is IDocCommentBlockNode)
            {
                IDocCommentBlockNode commentNode = node as IDocCommentBlockNode;

                // Rebuild the comment node
                String newXmlComments = RebuildComments(commentNode, option);

                newNode = CSharpElementFactory.GetInstance(project).CreateDocComment(newXmlComments);
            }
            else
            {
                String currentTypeName = node.GetText();
                String newTypeName = NodeAnalyser.GetNewTypeName(currentTypeName, option);

                // Create a new node depending of the original node type
                if (node is IReferenceExpressionNode
                    || node is ITypeUsageNode)
                {
                    newNode = CSharpElementFactory.GetInstance(project).CreateTypeUsageNode(newTypeName);
                }
                else if (node is IDeclaredTypeUsageNode)
                {
                    newNode = CSharpElementFactory.GetInstance(project).CreateDeclaredTypeUsageNode(newTypeName);
                }
                else if (node is IPredefinedTypeExpressionNode)
                {
                    // Ensure that we have the full CLR type name to create the reference expression
                    String clrTypeName = currentTypeName;

                    // Flip the type name as required
                    if (option == TypeAliasFormat.ConvertFromAlias)
                    {
                        clrTypeName = newTypeName;
                    }

                    IReferenceExpression expression =
                        CSharpElementFactory.GetInstance(project).CreateReferenceExpression(clrTypeName);

                    if (expression == null)
                    {
                        return;
                    }

                    IDeclaredType declaredType = GetDeclaredType(project, newTypeName);

                    if (declaredType != null)
                    {
                        // Tell ReSharper that this type usage points to System.String for example, so ReSharper core can insert using directives, or something else 
                        ITypeElement typeElement = declaredType.GetTypeElement();

                        if (typeElement != null)
                        {
                            expression.Reference.BindTo(typeElement);
                        }
                    }

                    newNode = expression.ToTreeNode();
                }
                else
                {
                    return;
                }
            }

            if (newNode == null)
            {
                // There is no node to work with
                return;
            }

            // replace AST fragment in actual file
            using (WriteLockCookie.Create(node.Parent.IsPhysical()))
            {
                ModificationUtil.ReplaceChild(node, newNode);
            }
        }

        /// <summary>
        /// Determines the type of the non numeric.
        /// </summary>
        /// <param name="predefinedType">
        /// Type of the predefined.
        /// </param>
        /// <param name="newTypeName">
        /// New name of the type.
        /// </param>
        /// <returns>
        /// An <see cref="IDeclaredType"/> instance or <c>null</c> if no matching type is found.
        /// </returns>
        private static IDeclaredType DetermineNonNumericType(PredefinedType predefinedType, String newTypeName)
        {
            switch (newTypeName)
            {
                case "object":
                case "Object":

                    return predefinedType.Object;

                case "char":
                case "Char":

                    return predefinedType.Char;

                case "string":
                case "String":

                    return predefinedType.String;

                case "bool":
                case "Boolean":

                    return predefinedType.Bool;

                default:

                    return null;
            }
        }

        /// <summary>
        /// Determines the type of the signed numeric.
        /// </summary>
        /// <param name="predefinedType">
        /// Type of the predefined.
        /// </param>
        /// <param name="newTypeName">
        /// New name of the type.
        /// </param>
        /// <returns>
        /// An <see cref="IDeclaredType"/> instance or <c>null</c> if no matching type is found.
        /// </returns>
        private static IDeclaredType DetermineSignedNumericType(PredefinedType predefinedType, String newTypeName)
        {
            switch (newTypeName)
            {
                case "byte":
                case "Byte":

                    return predefinedType.Byte;

                case "short":
                case "Int16":

                    return predefinedType.Short;

                case "int":
                case "Int32":

                    return predefinedType.Int;

                case "long":
                case "Int64":

                    return predefinedType.Long;

                case "double":
                case "Double":

                    return predefinedType.Double;

                case "decimal":
                case "Decimal":

                    return predefinedType.Decimal;

                case "float":
                case "Single":

                    return predefinedType.Float;

                case "sbyte":
                case "SByte":

                    return predefinedType.Sbyte;

                default:

                    return null;
            }
        }

        /// <summary>
        /// Determines the type of the unsigned numeric.
        /// </summary>
        /// <param name="predefinedType">
        /// Type of the predefined.
        /// </param>
        /// <param name="newTypeName">
        /// New name of the type.
        /// </param>
        /// <returns>
        /// An <see cref="IDeclaredType"/> instance or <c>null</c> if no matching type is found.
        /// </returns>
        private static IDeclaredType DetermineUnsignedNumericType(PredefinedType predefinedType, String newTypeName)
        {
            switch (newTypeName)
            {
                case "ushort":
                case "UInt16":

                    return predefinedType.Ushort;

                case "uint":
                case "UInt32":

                    return predefinedType.Uint;

                case "ulong":
                case "UInt64":

                    return predefinedType.Ulong;

                default:

                    return null;
            }
        }

        /// <summary>
        /// Gets the type of the declared.
        /// </summary>
        /// <param name="module">
        /// The module.
        /// </param>
        /// <param name="newTypeName">
        /// New name of the type.
        /// </param>
        /// <returns>
        /// An <see cref="IDeclaredType"/> instance or <c>null</c> if no matching type is found.
        /// </returns>
        private static IDeclaredType GetDeclaredType(IPsiModule module, String newTypeName)
        {
            PredefinedType predefinedType = PredefinedType.GetInstance(module);
            IDeclaredType declaredType = DetermineSignedNumericType(predefinedType, newTypeName);

            if (declaredType == null)
            {
                declaredType = DetermineUnsignedNumericType(predefinedType, newTypeName);
            }

            if (declaredType == null)
            {
                declaredType = DetermineNonNumericType(predefinedType, newTypeName);
            }

            return declaredType;
        }

        /// <summary>
        /// Rebuilds the comments.
        /// </summary>
        /// <param name="commentBlockNode">
        /// The comment block node.
        /// </param>
        /// <param name="option">
        /// The option.
        /// </param>
        /// <returns>
        /// A <see cref="String"/> instance.
        /// </returns>
        private static string RebuildComments(ITreeNode commentBlockNode, TypeAliasFormat option)
        {
            StringBuilder builder = new StringBuilder();
            List<IDocCRefElementNode> crefNodes = new List<IDocCRefElementNode>();

            foreach (IReference reference in commentBlockNode.GetFirstClassReferences())
            {
                IDocCRefElementNode crefNode = reference as IDocCRefElementNode;

                if (crefNode == null)
                {
                    continue;
                }

                crefNodes.Add(crefNode);
            }

            ITreeNode node = commentBlockNode.FirstChild;

            while (node != null)
            {
                IDocCommentNode commentNode = node as IDocCommentNode;

                if (commentNode != null)
                {
                    String nodeText = commentNode.CommentText.Trim();

                    foreach (IDocCRefElementNode crefNode in crefNodes)
                    {
                        String oldCrefText = crefNode.GetText();
                        String oldExpressionText = " cref=\"" + oldCrefText + "\"";

                        if (nodeText.Contains(oldExpressionText) == false)
                        {
                            continue;
                        }

                        TypeAliasFormat crefSuggestion = NodeAnalyser.GetFormatTypeSuggestion(crefNode, option);

                        if (crefSuggestion != option)
                        {
                            continue;
                        }

                        String newCrefText = NodeAnalyser.GetNewTypeName(oldCrefText, option);
                        String newExpressionText = oldExpressionText.Replace(oldCrefText, newCrefText);

                        // Update the comment text with the new expression
                        nodeText = nodeText.Replace(oldExpressionText, newExpressionText);
                    }

                    builder.AppendLine(nodeText);
                }

                node = node.NextSibling;
            }

            return builder.ToString();
        }

        /// <summary>
        /// Resolves the formattable nodes.
        /// </summary>
        /// <param name="node">
        /// The node to process.
        /// </param>
        /// <param name="rangeMarker">
        /// The range marker.
        /// </param>
        /// <param name="option">
        /// The option.
        /// </param>
        /// <param name="psiManager">
        /// The psi manager.
        /// </param>
        /// <param name="nodesToChange">
        /// The nodes to change.
        /// </param>
        /// <returns>
        /// An <see cref="ITreeNode"/> instance.
        /// </returns>
        private static ITreeNode ResolveFormattableNodesInternal(
            ITreeNode node,
            IPsiRangeMarker rangeMarker,
            TypeAliasFormat option,
            PsiManagerImpl psiManager,
            ICollection<ITreeNode> nodesToChange)
        {
            // Check if the node falls within the range or the document if there isn't a range
            if (rangeMarker == null || rangeMarker.IsValid == false
                || rangeMarker.DocumentRange.Contains(node.GetDocumentRange()))
            {
                // Get the format suggestion for this node
                TypeAliasFormat suggestion = NodeAnalyser.GetFormatTypeSuggestion(node, option);

                // Add the node to the change list if the suggestion is the same as the option specified
                if (suggestion == option)
                {
                    nodesToChange.Add(node);
                }
            }

            // Recurse through all the siblings and child nodes
            ITreeNode child = node.FirstChild;

            if (child == null)
            {
                return node;
            }

            // Loop through the siblings and resolve them
            while (true)
            {
                child = ResolveFormattableNodesInternal(child, rangeMarker, option, psiManager, nodesToChange);

                if (psiManager.IsCurrentTransactionFailed)
                {
                    return node;
                }

                if (child == null)
                {
                    return node;
                }

                ITreeNode nextSibling = child.NextSibling;

                if (nextSibling == null)
                {
                    return node;
                }

                child = nextSibling;
            }
        }
    }
}